-- Healer.lua — Sleek Combat Text (module Healer) — CLEAN (no minimap code)
-- • Soins : palette dédiée Cible vs Hors cible, par tier (pilotées par l’UI)
-- • Absorptions : texte "(valeur)" en jaune pâle (pas d’animation) + icônes (déduites des buffs)
-- • Test : toujours comme en “mode personnage”, simule une cible si besoin
-- • Anti-rouge Big/Ultra via Core (HotOverrideBegin/End)
-- • Dégâts : inchangés
-- ⚠️ Ce module NE crée/NE gère AUCUNE icône de minimap.

local ADDON, DFFT = ...
if not DFFT or not DFFT.DB then return end
local DB = DFFT.DB

DFFT.Healer = DFFT.Healer or {}
local M = DFFT.Healer

-- ===== Defaults =====
DB.healer = DB.healer or {}
local HDB = DB.healer
HDB.mode      = HDB.mode      or "ON"        -- forcé ON par défaut (AUTO posait souci sur Classic)
HDB.auto      = HDB.auto      or { window = 120, includeHOT = false, heuristic = false }
HDB.overheal  = HDB.overheal  or { show = true, threshold = 0.25, style = "SUFFIX" } -- suffix OH activé par défaut
-- AFTER
HDB.absorbs = HDB.absorbs or {}
if HDB.absorbs.show == nil then
  local v = (DB.showAbsorbs ~= nil) and DB.showAbsorbs
         or (DB.healer and DB.healer.absorbs and DB.healer.absorbs.show)
         or false  -- <- fallback default if truly nothing is set
  HDB.absorbs.show = not not v
end
HDB.absorbs.minDelta = HDB.absorbs.minDelta or 1

DB.debugLog   = DB.debugLog   or false

-- NEW: plus sign on heals enabled by default
if DB.healPlusEnabled == nil then DB.healPlusEnabled = true end
-- NEW: hide-only-direct-heals (NOT absorbs)
if DB.hideHeals == nil then DB.hideHeals = false end
-- NEW: hide overheal (default OFF = we DO show overheal)
if DB.hideOverheal == nil then DB.hideOverheal = false end

local function dbg(...) if DB.debugLog then print("|cff33ff99SleekCT-Heal|r", ...) end end

-- ===== Utils =====
local function clamp(x,a,b) if x<a then return a elseif x>b then return b end return x end
local function hex(h)
  h = (h or ""):gsub("#","")
  local r = tonumber(h:sub(1,2),16) or 255
  local g = tonumber(h:sub(3,4),16) or 255
  local b = tonumber(h:sub(5,6),16) or 255
  return {r/255, g/255, b/255}
end
local function shade(c, mul)
  c = c or {1,1,1}
  return { clamp((c[1] or 1)*mul,0,1), clamp((c[2] or 1)*mul,0,1), clamp((c[3] or 1)*mul,0,1) }
end

-- ===== Palettes dynamiques =====
local TARGET_PALETTE    = { small={.6,1,.7}, medium={.6,1,.7}, big={.6,1,.7}, vbig={.6,1,.7} }
local NONTARGET_PALETTE = { small={.5,.85,1}, medium={.5,.85,1}, big={.5,.85,1}, vbig={.5,.85,1} }
local ABS_COLOR         = {1,1,0.7}

local function rebuildFromDB()
  DB.healer = DB.healer or {}
  DB.healer.colors = DB.healer.colors or {}
  local HC = DB.healer.colors

  local tgtBase = (HC.target and (HC.target.medium or HC.target.big))
               or DB.healer.healMediumColor
               or hex("#a7f3d0")
  local ntBase  = (HC.nontarget and (HC.nontarget.medium or HC.nontarget.big)) or tgtBase

  TARGET_PALETTE.small  = shade(tgtBase, 1.10)
  TARGET_PALETTE.medium = shade(tgtBase, 1.00)
  TARGET_PALETTE.big    = shade(tgtBase, 0.90)
  TARGET_PALETTE.vbig   = shade(tgtBase, 0.80)

  NONTARGET_PALETTE.small  = shade(ntBase, 1.10)
  NONTARGET_PALETTE.medium = shade(ntBase, 1.00)
  NONTARGET_PALETTE.big    = shade(ntBase, 0.90)
  NONTARGET_PALETTE.vbig   = shade(ntBase, 0.80)

  local ac = HC.absorb or DB.healer.absorbColor or hex("#fef08a")
  ABS_COLOR = { ac[1] or 1, ac[2] or 0.95, ac[3] or 0.55 }
end

function M.RebuildColors() rebuildFromDB() end
M.RefreshPalette = M.RebuildColors

function M.SetAbsorbsEnabled(on)
  local onb = not not on
  HDB.absorbs = HDB.absorbs or {}
  HDB.absorbs.show = onb

  DB.healer = DB.healer or {}
  DB.healer.absorbs = DB.healer.absorbs or {}
  DB.healer.absorbs.show   = onb
  DB.healer.showAbsorbs    = onb
  DB.showAbsorbs           = onb
  DB.healer.absorbsEnabled = onb
end


function M.SetPlusEnabled(on)
  DB.healPlusEnabled = not not on
end

-- Hide-only-heals API (n'affecte PAS les absorbs)
function M.SetHealsHidden(on)
  DB.hideHeals = not not on
end
function M.AreHealsHidden()
  return (DB and DB.hideHeals == true) or false
end

-- NEW: Hide overheal API
function M.SetHideOverheal(on)
  DB.hideOverheal = not not on
end
function M.IsOverhealHidden()
  return DB.hideOverheal == true
end

rebuildFromDB()

-- ===== Formatage =====
local shortRounded = DFFT.shortNumberRounded or function(n)
  n = math.max(0, math.floor((tonumber(n) or 0) + 0.5))
  if n < 1000 then return tostring(n) end
  if n < 1000000 then return string.format("%dk", math.floor(n/1000 + 0.5)) end
  local m = n/1000000
  local t = math.floor(m*10 + 0.5)/10
  if math.abs(t - math.floor(t)) < 1e-6 then return string.format("%dm", math.floor(t + 0.5)) end
  return string.format("%.1fm", t)
end
local short = DFFT.shortNumber or shortRounded

-- ===== Rôle actif (conservé pour l’API, mais on ne hard-gate plus) =====
local function isHealerActive()
  if HDB.mode == "ON"  then return true end
  if HDB.mode == "OFF" then return false end
  local spec = GetSpecialization and GetSpecialization()
  return spec and (GetSpecializationRole(spec) == "HEALER") or false
end

-- ===== Forçages TEST =====
M._forcePlayerAnchor_test = false
M._forcedTargetGUID       = nil

local function ownerIsTarget(dstGUID)
  if M._forcedTargetGUID then
    return (dstGUID == M._forcedTargetGUID)
  end

  -- NEW: si le soin est sur moi-même, on force true
  local me = UnitGUID("player")
  if me and dstGUID and dstGUID == me then
    return true
  end

  local t = UnitGUID("target")
  return (t and dstGUID and t == dstGUID) and true or false
end


-- ===== Auto-calibration =====
local samples, times, head, size = {}, {}, 0, 0
local function windowSec() return (HDB.auto and HDB.auto.window) or 120 end
local function pushSample(v)
  head = head + 1; samples[head]=v; times[head]=GetTime(); size = size + 1
  local cutoff = GetTime() - windowSec()
  for i = head - size + 1, head do
    local t = times[i]; if not t or t >= cutoff then break else samples[i]=nil; times[i]=nil end
  end
  local n=0; for i=head - size + 1, head do if samples[i]~=nil then n=n+1 end end
  size = n
end
local function percentile(sorted, p)
  local n = #sorted; if n == 0 then return 0 end
  local k = math.max(1, math.min(n, math.floor(n*p + 0.5)))
  return sorted[k]
end
local function chooseTier(eff, isHOT)
  if HDB.auto and HDB.auto.heuristic then
    local maxv = 0; for i=head - size + 1, head do local v=samples[i]; if v and v>maxv then maxv=v end end
    if maxv <= 0 then return (eff>0 and "medium" or "small") end
    local r = eff/maxv
    if     r < 0.15 then return "small"
    elseif r < 0.35 then return "medium"
    elseif r < 0.65 then return "big"
    else return "vbig" end
  end
  local includeHOT = (HDB.auto and HDB.auto.includeHOT) or false
  if eff>0 and ((not isHOT) or includeHOT) then pushSample(eff) end
  local arr = {}; for i=head - size + 1, head do local v=samples[i]; if v then arr[#arr+1]=v end end
  table.sort(arr)
  if #arr == 0 then return (eff>0 and "medium" or "small") end
  local p60, p85, p97 = percentile(arr,0.60), percentile(arr,0.85), percentile(arr,0.97)
  if     eff < p60 then return "small"
  elseif eff < p85 then return "medium"
  elseif eff < p97 then return "big"
  else return "vbig" end
end

-- ===== Texture d'icône (robuste) =====
local function SpellIcon(id)
  if C_Spell and C_Spell.GetSpellInfo then
    local info = C_Spell.GetSpellInfo(id); if info and info.iconID then return info.iconID end
  end
  if GetSpellTexture then
    local tex = GetSpellTexture(id); if tex then return tex end
  end
  if GetSpellInfo then
    local _,_,icon = GetSpellInfo(id); if icon then return icon end
  end
  return nil
end

-- ===== Mémo d’icône pour absorbs =====
local recentAbsorbIcon = {}           -- [dstGUID] = { icon = fileID, t = GetTime() }
local ABSORB_FALLBACK_ICON = 135940   -- PW:S fileID (fallback)
local ABSORB_ICON_TTL = 1.2           -- fenêtre (sec) entre aura et hausse d’absorb

-- ===== Palette par appel =====
local function withHealPaletteFor(tier, isTarget, fn)
  rebuildFromDB()
  local b_small, b_med, b_big, b_vbig = DB.smallColor, DB.mediumColor, DB.bigColor, DB.veryBigColor
  local b_nonTarget = DB.nonTargetColor

  local P = isTarget and TARGET_PALETTE or NONTARGET_PALETTE
  DB.smallColor   = {P.small[1],  P.small[2],  P.small[3]}
  DB.mediumColor  = {P.medium[1], P.medium[2], P.medium[3]}
  DB.bigColor     = {P.big[1],    P.big[2],    P.big[3]}
  DB.veryBigColor = {P.vbig[1],   P.vbig[2],   P.vbig[3]}

  if not isTarget then
    local nt = P[tier] or P.small
    DB.nonTargetColor = {nt[1], nt[2], nt[3]}
  end

  local ok, err = pcall(fn)

  DB.smallColor, DB.mediumColor, DB.bigColor, DB.veryBigColor = b_small, b_med, b_big, b_vbig
  DB.nonTargetColor = b_nonTarget
  if not ok then dbg("Show failed:", err) end
end

-- ===== Overheal suffix =====
local function applyOverhealSuffix(txt, raw, overheal)
  local opt = HDB.overheal
  if not (opt and opt.show) then return txt end
  local oh = math.max(0, tonumber(overheal) or 0)
  local total = math.max(1, tonumber(raw) or 1)
  local ratio = oh / total
  local thr = opt.threshold or 0.25
  if ratio <= thr then return txt end
  local pct = math.floor(ratio*100 + 0.5)
  return string.format("%s (~%d%% OH)", txt, pct)
end

-- ===== Ancrage test =====
local function withForcedPlayerAnchor(fn)
  if not M._forcePlayerAnchor_test then return fn() end
  local prev = DB.globalAnchorToTarget
  DB.globalAnchorToTarget = false
  local ok, err = pcall(fn)
  DB.globalAnchorToTarget = prev
  if not ok then dbg("anchor-show err:", err) end
end

-- ===== Affichages =====
local function showHealText(eff, txt, tier, isTarget, isCrit, opts)
  withHealPaletteFor(tier, isTarget, function()
    local function doShow()
      if DFFT.showNumber then DFFT.showNumber(eff, txt, tier, isTarget, isCrit, nil, opts) end
    end
    if tier == "big" or tier == "vbig" then
      local P = isTarget and TARGET_PALETTE or NONTARGET_PALETTE
      local pulseRGB = P[tier] or P.big
      if DFFT.HotOverrideBegin then DFFT.HotOverrideBegin(pulseRGB) end
      withForcedPlayerAnchor(doShow)
      local dur = math.max(
        type(DB.critPulseDuration)=="number" and DB.critPulseDuration or 0,
        type(DB.fadeOutTime)=="number"       and DB.fadeOutTime       or 0,
        1.2
      ) + 0.4
      if DFFT.HotOverrideEnd then C_Timer.After(dur, DFFT.HotOverrideEnd) end
    else
      withForcedPlayerAnchor(doShow)
    end
  end)
end

-- Absorbs (couleur unique)
local function withAbsorbPalette(fn)
  rebuildFromDB()
  local b_small, b_med, b_big, b_vbig = DB.smallColor, DB.mediumColor, DB.bigColor, DB.veryBigColor
  local b_nonTarget = DB.nonTargetColor
  DB.smallColor, DB.mediumColor, DB.bigColor, DB.veryBigColor = ABS_COLOR, ABS_COLOR, ABS_COLOR, ABS_COLOR
  DB.nonTargetColor = ABS_COLOR
  local ok, err = pcall(fn)
  DB.smallColor, DB.mediumColor, DB.bigColor, DB.veryBigColor = b_small, b_med, b_big, b_vbig
  DB.nonTargetColor = b_nonTarget
  if not ok then dbg("abs show err:", err) end
end

local function showAbsorbText(eff, txt, tier, isTarget, opts)
  withAbsorbPalette(function()
    local function doShow()
      if DFFT.showNumber then DFFT.showNumber(eff, txt, tier, isTarget, false, nil, opts) end
    end
    if tier == "big" or tier == "vbig" then
      if DFFT.HotOverrideBegin then DFFT.HotOverrideBegin(ABS_COLOR) end
      withForcedPlayerAnchor(doShow)
      local dur = math.max(
        type(DB.critPulseDuration)=="number" and DB.critPulseDuration or 0,
        type(DB.fadeOutTime)=="number"       and DB.fadeOutTime       or 0,
        1.2
      ) + 0.4
      if DFFT.HotOverrideEnd then C_Timer.After(dur, DFFT.HotOverrideEnd) end
    else
      withForcedPlayerAnchor(doShow)
    end
  end)
end

-- ===== Emissions =====
local function emitHeal(dstGUID, amount, overheal, isCrit, isHOT, opts)
  -- Hide-only-heals : court-circuite l'affichage des soins (n’affecte pas les absorbs)
  if M.AreHealsHidden() then return end

  local amt = tonumber(amount) or 0
  local oh  = tonumber(overheal) or 0
  local eff = math.max(0, amt - oh)

  -- --- NEW: gestion de l’overheal ---
  if eff <= 0 then
    -- Pure overheal
    if DB.hideOverheal then
      return -- masqué si l’option est activée
    else
      -- On l’affiche: on garde le texte basé sur le heal brut,
      -- mais on réduit l’ampleur de l’animation pour éviter un énorme “+500k” en pleine vie.
      local animEff = math.max(1, math.floor(amt * 0.35 + 0.5))  -- taille visuelle modérée
      local tier = chooseTier(animEff, isHOT)
      local isTarget = ownerIsTarget(dstGUID)
      local nstr = (tier == "small" or tier == "medium") and shortRounded(amt) or short(amt)
      local txt = (DB.healPlusEnabled ~= false) and ("+" .. nstr) or nstr
      txt = applyOverhealSuffix(txt, amt, oh) -- indiquera ~100% OH
      showHealText(animEff, txt, tier, isTarget, isCrit, opts)
      return
    end
  end

  -- Heals avec effet réel
  local tier = chooseTier(eff, isHOT)
  local isTarget = ownerIsTarget(dstGUID)

  local nstr = (tier == "small" or tier == "medium") and shortRounded(eff) or short(eff)
  local txt = (DB.healPlusEnabled ~= false) and ("+" .. nstr) or nstr
  txt = applyOverhealSuffix(txt, amt, oh)

  showHealText(eff, txt, tier, isTarget, isCrit, opts)
end

-- Absorption : delta positif de UnitGetTotalAbsorbs(unit)
local function emitAbsorb(dstGUID, amount, icon)
  if not (HDB.absorbs and HDB.absorbs.show) then return end
  local eff = tonumber(amount) or 0
  if eff <= (HDB.absorbs.minDelta or 1) then return end
  local tier = chooseTier(eff, false)
  local isTarget = ownerIsTarget(dstGUID)
  local nstr = (tier=="small" or tier=="medium") and shortRounded(eff) or short(eff)
  local txt = "(" .. nstr .. ")"
  local opts = icon and { icon = icon } or nil
  if icon then DFFT._iconForNext = icon end -- filet au cas où
  showAbsorbText(eff, txt, tier, isTarget, opts)
end

-- ===== Source = joueur / pet =====
local playerGUID, petGUID
local function isFromMe(srcGUID)
  if not srcGUID then return false end
  if srcGUID == playerGUID then return true end
  if DB.includePet and petGUID and srcGUID == petGUID then return true end
  return false
end

-- ===== Absorbs tracking =====
local lastAbs = { player = 0, target = 0 }
local function scanAbs(unit)
  local guid = UnitGUID(unit)
  if not guid then return end
  local now = UnitGetTotalAbsorbs and UnitGetTotalAbsorbs(unit) or 0
  local prev = lastAbs[unit] or 0
  if now > prev then
    local icon
    local rec = recentAbsorbIcon[guid]
    if rec and (GetTime() - (rec.t or 0)) < ABSORB_ICON_TTL then
      icon = rec.icon
    end
    emitAbsorb(guid, now - prev, icon or ABSORB_FALLBACK_ICON)
  end
  lastAbs[unit] = now
end

-- ===== Events =====
local f = M._frame or CreateFrame("Frame")
M._frame = f
f:SetScript("OnEvent", function(_, event, ...)
  if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
    playerGUID = UnitGUID("player"); petGUID = UnitGUID("pet")
    lastAbs.player = (UnitGetTotalAbsorbs and UnitGetTotalAbsorbs("player")) or 0
    if UnitExists("target") then
      lastAbs.target = (UnitGetTotalAbsorbs and UnitGetTotalAbsorbs("target")) or 0
    end
    rebuildFromDB()

  elseif event == "UNIT_PET" then
    if ... == "player" then petGUID = UnitGUID("pet") end

  elseif event == "PLAYER_TARGET_CHANGED" then
    lastAbs.target = (UnitGetTotalAbsorbs and UnitGetTotalAbsorbs("target")) or 0

  elseif event == "UNIT_ABSORB_AMOUNT_CHANGED" then
    local unit = ...
    if unit == "player" or unit == "target" then scanAbs(unit) end

  elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
    -- IMPORTANT: ne jamais bloquer sur AUTO/role ici. On bloque UNIQUEMENT si l’utilisateur force OFF.
    if HDB.mode == "OFF" then return end

    local _, sub,
      _, srcGUID, _, _, _,
      dstGUID, _, _, _,
      spellId, _, _, amount, overheal, _, critical = CombatLogGetCurrentEventInfo()

    -- Soins : icônes directes via opts
    if sub == "SPELL_HEAL" or sub == "SPELL_PERIODIC_HEAL" then
      if isFromMe(srcGUID) then
        if M.AreHealsHidden() then return end
        local icon = SpellIcon(spellId)
        local opts = icon and { icon = icon } or nil
        if icon then DFFT._iconForNext = icon end
        emitHeal(dstGUID, amount, overheal, not not critical, sub == "SPELL_PERIODIC_HEAL", opts)
      end
    end

    -- Pont pour absorbs : mémorise l'icône de la dernière aura appliquée/rafraîchie par le joueur
    if (sub == "SPELL_AURA_APPLIED" or sub == "SPELL_AURA_REFRESH") then
      if isFromMe(srcGUID) and dstGUID and spellId then
        local icon = SpellIcon(spellId)
        if icon then
          recentAbsorbIcon[dstGUID] = { icon = icon, t = GetTime() }
        end
      end
    end
  end
end)
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("UNIT_PET")
f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
f:RegisterEvent("PLAYER_TARGET_CHANGED")
f:RegisterEvent("UNIT_ABSORB_AMOUNT_CHANGED")

-- ===== Test API =====
local function testBurstFor(dstGUID)
  if M.AreHealsHidden() then return end
  -- Heals (avec icônes)
  C_Timer.After(0.05,  function() emitHeal(dstGUID,  1800,   0,    false, false, {icon=134914}) end)  -- Small
  C_Timer.After(0.15,  function() emitHeal(dstGUID, 22000,  1500,  false, false, {icon=135905}) end)  -- Medium
  C_Timer.After(0.25,  function() emitHeal(dstGUID, 12000,  2000,  false, true,  {icon=135953}) end)  -- HoT tick
  C_Timer.After(0.40,  function() emitHeal(dstGUID, 95000,  5000,  true,  false, {icon=135907}) end)  -- Big crit
  C_Timer.After(0.60,  function() emitHeal(dstGUID, 420000, 20000, true,  false, {icon=135928}) end)  -- Ultra crit
  -- Absorb (avec icône de PW:S en fallback)
  C_Timer.After(0.80,  function() emitAbsorb(dstGUID, 38000, ABSORB_FALLBACK_ICON) end)
end

function M.TestHeals()
  if M.AreHealsHidden() then return end
  M._forcePlayerAnchor_test = true
  local p = UnitGUID("player")
  local t = UnitGUID("target")
  if t then
    testBurstFor(t)
    C_Timer.After(1.2, function() testBurstFor(p) end)
    C_Timer.After(2.8, function() M._forcePlayerAnchor_test = false end)
  else
    local fake = "SCT_FAKE_TARGET_GUID"
    M._forcedTargetGUID = fake
    testBurstFor(fake)
    C_Timer.After(1.2, function()
      testBurstFor(p)
      C_Timer.After(1.6, function()
        M._forcedTargetGUID = nil
        M._forcePlayerAnchor_test = false
      end)
    end)
  end
end
M.TestShowHeals = M.TestHeals
